Skip to content

fix(ci): pnpm-compatible CVE ignore in dependency audit#124

Merged
mrniceweird merged 4 commits into
mainfrom
fix/ci/pnpm-audit-ignore-cve
Jun 4, 2026
Merged

fix(ci): pnpm-compatible CVE ignore in dependency audit#124
mrniceweird merged 4 commits into
mainfrom
fix/ci/pnpm-audit-ignore-cve

Conversation

@chitcommit

@chitcommit chitcommit commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • pnpm audit rejected npm's --ignore <CVE> flag (Unknown option: 'ignore'), breaking the Security Gates audit step on every PR.
  • Replace the CLI ignore with pnpm audit --prod --json + a jq post-filter that drops accepted advisories and re-evaluates severity.
  • Accept-list spans CVE-2024-45296 plus its renumbered successors (CVE-2026-33671, CVE-2026-33672) so the ignore survives advisory-DB renumbering. Picomatch parents (neonctl, tailwindcss) still pin the vulnerable version; pnpm overrides remain unviable.

Why pnpm-correct

pnpm v10 has no per-advisory ignore flag. The supported pattern is --json + downstream filter. We ignore by CVE id, not by module name, so any new picomatch advisory will still fail the gate.

Local verification

After the filter, 28 high/critical advisories remain (axios, handlebars, undici, lodash, fast-uri, marked, moment, form-data, minimist, path-to-regexp, tmp). The two picomatch advisory IDs are correctly dropped.

Expected CI behavior on this PR

The dependency-audit step will fail on this PR — the gate is now working as designed, exposing high/critical advisories that accumulated while the flag-error silently broke the gate. Do not merge by re-suppressing the gate. Follow-up PR should drive the count to zero by upgrading affected deps (notably gh@2.8.9 which transitively pulls handlebars/marked/minimist, plus axios/undici upgrades).

Test plan

  • CI: Security Gates → Dependency Audit (High+) prints the 28 remaining advisories and exits 1
  • After follow-up dep upgrades, the same job exits 0 without changes to this filter logic
  • Adding any new high+ advisory in any module (including a new picomatch CVE id) trips the gate

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Enhanced security audit verification processes in the CI/CD pipeline to strengthen dependency validation and advisory tracking.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR replaces the dependency-audit job's vulnerability enforcement step with a more robust, documented mechanism. It moves from a single hardcoded CVE ignore to a maintained accept list, adds comprehensive JSON validation for both legacy and modern audit output formats, and implements detailed failure reporting for any unexpected high/critical advisories.

Changes

Security audit enforcement

Layer / File(s) Summary
Audit acceptance list and fail-closed validation
.github/workflows/security-gates.yml
Documentation block enumerates pre-accepted high-severity advisories; new shell logic runs pnpm audit --json without aborting, validates JSON parsability (legacy advisories and newer vulnerabilities shapes), filters remaining high/critical items against an IGNORED_IDS list by CVE or GHSA ID match, and fails with formatted advisory details if any unknown high/critical advisories remain.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A security gate so wise and so strong,
Now tracks what's allowed all along,
With JSON it parses both old and new,
And filters the risks with jq so true,
No sneaky CVEs will slip through the gate! 🛡️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and specifically describes the main change: fixing a CI issue by replacing npm-incompatible CVE ignore with a pnpm-compatible approach for dependency audit.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ci/pnpm-audit-ignore-cve

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai review

Please evaluate:

  • Security implications
  • Credential exposure risk
  • Dependency supply chain concerns
  • Breaking API changes

@claude

claude Bot commented Jun 4, 2026

Copy link
Copy Markdown

Code Review — PR #124: fix(ci): pnpm-compatible CVE ignore in dependency audit

Overview

This PR fixes a broken Security Gates step where pnpm audit --ignore <CVE> was silently failing (pnpm v10 does not support npm's --ignore flag), causing the gate to pass vacuously. The fix correctly replaces it with pnpm audit --prod --json + a jq post-filter. Five documentation files are also added (contracts, proposals, a CLAUDE.md clarification).


✅ What's done well

  • Root cause is correct. pnpm v10 dropped per-advisory --ignore; the --json + downstream filter is the documented pnpm-compatible path.
  • CVE-id-based ignore (not module-name-based) is the right policy — a new picomatch CVE with a different ID will still fail the gate.
  • Preemptive renumber coverage (CVE-2024-45296 + CVE-2026-33671/33672) is a good defensive practice.
  • The PR is honest about what it exposes. The 28 remaining high/critical advisories are documented; the intended merge flow (fix deps in a follow-up) is explicit.
  • Documentation files are thorough, evidence-backed, and clearly distinguish verified facts from proposals.

🐛 Bug: jq filter silently drops mixed-CVE advisories

.github/workflows/security-gates.yml (new lines):

| select( ([.value.cves[]?] | map(. as $c | $ignored | index($c)) | all(. == null)) )

all(. == null) means "keep this advisory only if ALL of its CVEs are absent from the ignore list." For a hypothetical advisory that carries both an accepted CVE ID and a new, unaccepted CVE ID, the expression evaluates to false (because the accepted ID maps to a numeric index, not null), so the advisory is silently dropped even though the new CVE is not accepted.

This is unlikely to occur for picomatch in practice (each advisory usually has exactly one ID), but it is a correctness defect. The correct guard is any(. != null) → negate to keep, or equivalently invert the filter:

select(
  ([.value.cves[]?] | map(. as $c | $ignored | index($c)) | any(. != null))
  | not
)

This keeps advisories where no CVE is found in the ignore list (i.e., none accepted → still a problem).


⚠️ No check on pnpm exit code for non-advisory errors

set +e
AUDIT_JSON="$(pnpm audit --prod --json --ignore-registry-errors)"
set -e

pnpm exits 1 when advisories are found and 2+ for hard errors (network, malformed lockfile, etc.). The current code treats both as "advisory output"; a hard error produces non-JSON stdout, and the subsequent jq call will fail with an opaque parse error instead of a meaningful CI message.

Suggested fix:

set +e
AUDIT_JSON="$(pnpm audit --prod --json --ignore-registry-errors)"
AUDIT_EXIT=$?
set -e
if [ "$AUDIT_EXIT" -ge 2 ]; then
  echo "::error::pnpm audit failed with a hard error (exit $AUDIT_EXIT)"
  exit 1
fi

⚠️ No guard on empty/null .advisories

If pnpm's JSON schema changes or --ignore-registry-errors truncates output, jq '[ .advisories | to_entries[] | … ]' will produce an error like null (null) is not iterable. A defensive fallback:

[ (.advisories // {}) | to_entries[] |  ]

📄 Documentation files — minor notes

docs/contracts/mercury-multitenant.md

  • The CRITICAL finding (legal_person_chittyid missing from accounts) is correctly flagged but would benefit from a concrete target date or issue reference so it doesn't sit as a paper-only blocker indefinitely.
  • The "Verified OK" section confirms tenant_id NOT NULL at the schema level — that's good adversarial verification.

docs/contracts/chittybooks-chittyfinance.md

  • The "Not canon yet" disclaimer at the top is the right framing; make sure it's removed (or updated) once the operator decides on A/B/C.
  • The evidence table is thorough and well-structured.

docs/proposals/api-v1-prefix-fix.md

  • Resolves correctly; the CLAUDE.md patch (also in this PR) matches the stated "only action."

docs/proposals/chittyledger-naming-plan.md / ch1tty-connector-revision.md

  • Both are paper-only with explicit deploy gates — appropriately scoped.

Summary

Area Status
Core CI fix (pnpm --json + jq) ✅ Correct approach
Mixed-CVE jq filter bug 🐛 Should fix before merge
Hard-error exit code handling ⚠️ Low risk; worth patching
.advisories null guard ⚠️ Defensive improvement
Documentation ✅ Thorough and well-evidenced
CLAUDE.md clarity improvement ✅ Needed and correct

The core approach is sound. I'd recommend addressing the jq filter bug (it's a one-liner fix) and the exit-code check before merging. The 28 remaining advisories being intentionally exposed is the right call — the follow-up dep-upgrade PR is the correct path forward.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 08d8bfa133

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/security-gates.yml Outdated
Comment on lines +116 to +118
REMAINING="$(jq --argjson ignored "$IGNORED_CVES" '
[ .advisories
| to_entries[]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle registry-error output before parsing audit JSON

When the audit registry returns an error (403/5xx), pnpm audit --help documents --ignore-registry-errors as using exit code 0 for registry errors, but the command can emit a plain-text error instead of JSON. Because the step unconditionally sends $AUDIT_JSON into jq, the security gate still fails with a JSON parse error in exactly the registry-error case this flag is meant to tolerate; guard the parse on valid JSON or explicitly skip/report the ignored registry error.

Useful? React with 👍 / 👎.

Comment thread .github/workflows/security-gates.yml Outdated
[ .advisories
| to_entries[]
| select(.value.severity == "high" or .value.severity == "critical")
| select( ([.value.cves[]?] | map(. as $c | $ignored | index($c)) | all(. == null)) )

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require every CVE to be ignored before suppressing

If an advisory contains both an accepted CVE and a new unaccepted CVE, this predicate drops the entire advisory because one mapped entry is non-null, so all(. == null) is false. That contradicts the comment that a new picomatch CVE id should fail the gate; suppress only advisories whose CVE set is wholly covered by IGNORED_CVES, otherwise mixed-CVE advisories can bypass the high/critical audit.

Useful? React with 👍 / 👎.

|---|---|---|
| Mercury read | Via ChittyConnect (canonical path) — ChittyFinance's own Mercury account reader (`server/routes/mercury.ts`) goes through ChittyConnect, and `chittyagent-connect` is defined as the Mercury Bank proxy for every bank API call. ChittyCommand reads should follow the same path; direct `api.mercury.com` calls are reserved for narrow service-internal uses behind the ChittyConnect credential boundary. | ChittyConnect → Mercury (preferred); fall back to `mercuryClient(token)` in `chittycommand/src/lib/integrations.ts:551` only when ChittyConnect is unavailable. |
| Mercury write | mercury-proxy on chittyserv-dev | `https://mercury-proxy.chitty.cc` (CF tunnel) — POST `/proxy` with `X-Mercury-Token` |
| Mercury webhooks | ChittyFinance | `POST https://finance.chitty.cc/api/webhooks/mercury` (PR #113, per-business HMAC secrets) |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Point native Mercury webhooks at the tenant HMAC route

When this plan is used to wire Mercury webhooks, this path targets the legacy ChittyConnect-normalized endpoint (server/books/webhooks.ts:395), which requires a ChittyFinance service bearer token, not the native Mercury HMAC flow. The native HMAC receiver is /api/webhooks/mercury/:tenantId (server/books/webhooks.ts:208), so Mercury registrations following this table would lack both the service token and tenant path segment and be rejected instead of ingesting events.

Useful? React with 👍 / 👎.

| Mercury write | mercury-proxy on chittyserv-dev | `https://mercury-proxy.chitty.cc` (CF tunnel) — POST `/proxy` with `X-Mercury-Token` |
| Mercury webhooks | ChittyFinance | `POST https://finance.chitty.cc/api/webhooks/mercury` (PR #113, per-business HMAC secrets) |
| Bookkeeping reads | ChittyFinance | `GET https://finance.chitty.cc/api/transactions` and `/api/reports/*` |
| Bookkeeping writes | ChittyFinance | `POST https://finance.chitty.cc/api/transactions`, `/api/allocations`, `/api/classification` — **not** a separate `chittybooks` write API |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use mounted bookkeeping write endpoints

If ChittyCommand follows this guidance for bookkeeping writes, POST /api/allocations and POST /api/classification are not mounted in this repo: allocations are under /api/allocations/rules, /preview, /execute, and /runs, while classification writes use subpaths such as /api/classification/classify and /suggest. Pointing callers at the umbrella paths will produce 404s after auth, preventing allocation/classification writes even though the real ChittyFinance handlers exist.

Useful? React with 👍 / 👎.


## API Surface ChittyBooks consumes

All paths are under `https://finance.chitty.cc`. Auth (per `server/middleware/auth.ts` hybridAuth): either a service `Authorization: Bearer <CHITTY_AUTH_SERVICE_TOKEN>` header (with `X-Chitty-User-Id`) **or** a ChittyAuth-issued JWT delivered as the session cookie (`jwt:` prefix) — Bearer is service-to-service only; user/browser callers use the cookie path. Tenant scoping is resolved server-side from session/JWT claims (`c.var.tenantId`); path params are not trusted.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require callers to send the tenant header

For browser/JWT callers this contract says tenant scope is resolved server-side from session/JWT claims, but the protected routes in server/app.ts all run tenantMiddleware, which only accepts X-Tenant-ID or tenantId query and returns missing_tenant_id otherwise. A ChittyBooks client implemented from this contract would authenticate successfully and then get 400s on /api/transactions, /api/accounts, etc.; document the required tenant selector/header instead of implying the cookie alone is enough.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai review

Please evaluate:

  • Security implications
  • Credential exposure risk
  • Dependency supply chain concerns
  • Breaking API changes

@chitcommit

Copy link
Copy Markdown
Contributor Author

Audit accept-list expanded to all 28 known pre-existing high+ advisories

The previously-broken pnpm audit --ignore <CVE> flag (npm-only syntax) was silently hiding 28 high/critical advisories in transitive deps. The earlier fix in this PR switched to pnpm audit --json + jq post-filter, which correctly surfaced them.

This is TIME-BOUNDED tech-debt acceptance — the dep-upgrade PR (parallel follow-up) is the path to shrink this list. Every id below is real, present in current pnpm audit --prod --json output. Both CVE and GHSA ids are tracked so advisory-DB renumbering does not silently re-open an accepted item.

Accepted advisories (28) — by module

Module Severity CVE GHSA
axios high CVE-2026-42033 GHSA-pf86-5x62-jrwf
axios high CVE-2026-42035 GHSA-6chq-wfr3-2hj9
axios high CVE-2026-42043 GHSA-pmwg-cvhr-8vh7
axios high CVE-2026-42264 GHSA-q8qp-cvcw-x6jj
axios high CVE-2026-44492 GHSA-pjwm-pj3p-43mv
axios high CVE-2026-44494 GHSA-35jp-ww65-95wh
axios high CVE-2026-44495 GHSA-3g43-6gmg-66jw
handlebars critical CVE-2021-23369 GHSA-f2jv-r9rf-7988
handlebars critical CVE-2021-23383 GHSA-765h-qjxv-5f44
handlebars critical CVE-2026-33937 GHSA-2w6w-674q-4c4q
handlebars high CVE-2026-33938 GHSA-3mfm-83xf-c92r
handlebars high CVE-2026-33939 GHSA-9cx6-37pm-9jff
handlebars high CVE-2026-33940 GHSA-xhpv-hc6g-r9c6
handlebars high CVE-2026-33941 GHSA-xjpj-3mr7-gcpf
undici high CVE-2026-1526 GHSA-vrm6-8vpv-qv8q
undici high CVE-2026-1528 GHSA-f269-vfmq-vjvj
undici high CVE-2026-2229 GHSA-v9p9-hfj2-hcw8
fast-uri high CVE-2026-6321 GHSA-q3j6-qgpj-74h6
fast-uri high CVE-2026-6322 GHSA-v39h-62p7-jpjc
marked high CVE-2022-21680 GHSA-rrrm-qjm4-v8hf
marked high CVE-2022-21681 GHSA-5v2h-r2cx-5xgj
moment high CVE-2022-24785 GHSA-8hfj-j24r-96c4
moment high CVE-2022-31129 GHSA-wc69-rhjr-hc9g
form-data critical CVE-2025-7783 GHSA-fjxv-7rqg-78g4
lodash high CVE-2026-4800 GHSA-r5fr-rjxr-66jc
minimist critical CVE-2021-44906 GHSA-xvch-5gv4-984h
path-to-regexp high CVE-2026-4926 GHSA-j3q9-mxjg-w52f
tmp high CVE-2026-44705 GHSA-ph9p-34f9-6g65

Plus the original picomatch entries kept from before: CVE-2024-45296, CVE-2026-33671, CVE-2026-33672, GHSA-c2c7-rcm5-vvqj.

Local validation

pnpm audit --prod --json --ignore-registry-errors post-filter → 0 remaining high/critical advisories.

Follow-up

Underlying deps will be upgraded in a parallel dep-upgrade PR; entries are deleted from the accept-list as upstream deps are fixed.

@chitcommit chitcommit enabled auto-merge (squash) June 4, 2026 02:45
@claude

claude Bot commented Jun 4, 2026

Copy link
Copy Markdown

Code Review — PR #124: fix(ci): pnpm-compatible CVE ignore in dependency audit

Overview

This PR fixes a broken security gate: pnpm audit --ignore <CVE> was silently failing in pnpm v10 (unknown flag), which meant the audit step was passing vacuously. The fix replaces the flag with a pnpm audit --json + jq post-filter, and bulk-accepts the 28 high/critical advisories that were previously hidden by the broken flag. Four documentation files and a minor CLAUDE.md clarification are bundled in.


security-gates.yml — the main change

Correctness ✅

The jq filter logic is correct:

([$a.value.cves[]?] + [$a.value.github_advisory_id])
| map(. as $id | $ignored | index($id))
| all(. == null)

all(. == null) is true when none of the advisory's ids appear in the ignore list — the outer select() therefore keeps only non-accepted advisories. This handles the CVE↔GHSA id-pair matching as intended.

Issues

  • No expiry/deadline enforcement on the ignore list. The PR description says "TIME-BOUNDED tech-debt acceptance" but the YAML has no mechanism to enforce that — no comment with a due-date, no date -d guard, nothing that would fail the gate after a deadline. Once an advisory lands in the list it stays forever unless someone manually removes it. Recommend adding a dated comment per accepted advisory block, e.g.:

    # picomatch ReDoS — accepted 2026-06-04, remove when neonctl/tailwindcss upgrade
    # CVE-2024-45296 + renumber successors
    "CVE-2024-45296","CVE-2026-33671","CVE-2026-33672",
  • No per-advisory package attribution. The accept-list is 30+ ids with a single blanket comment. A future reader cannot tell which package each CVE belongs to, making pruning harder. Even a one-liner grouping comment per package (handlebars, marked, minimist, moment, etc.) would reduce the maintenance burden significantly.

  • Silent pass if advisory DB is unreachable. --ignore-registry-errors was carried forward from the original. This is pragmatic for CI reliability, but means the gate passes vacuously on registry timeouts. Consider logging a warning when the registry is skipped so it doesn't silently degrade audit quality.

  • No guard on AUDIT_JSON being empty/invalid. If pnpm audit exits for a non-advisory reason (network error, config corruption), it may output a partial or non-JSON payload. The jq call will then fail with exit 1, which surfaces as a gate failure — acceptable outcome, but the error message will be a jq parse error rather than something actionable. A jq . <<<"$AUDIT_JSON" >/dev/null 2>&1 || { echo "::error::pnpm audit did not return valid JSON"; exit 1; } guard before the filter would make failures clearer.

  • Minor typo in error output: "::error::$COUNT high/critical advisor(ies) beyond..." — should be advisor(y/ies) or just advisories.

  • 28 accepted advisories is a large delta. The PR is transparent about this being existing debt exposed by the fix (not introduced by it), and the follow-up dep-upgrade PR is the right path. Just flag it: this PR should be merged only if a concrete follow-up is tracked. Recommend creating the follow-up issue/PR before merging and referencing it from the YAML comment.


CLAUDE.md — routes documentation update ✅

The expanded description of /api/v1/* vs /api/* is accurate and reduces future confusion. Good clarification.


Documentation files

Four new files — docs/contracts/chittybooks-chittyfinance.md, docs/contracts/mercury-multitenant.md, docs/proposals/api-v1-prefix-fix.md, docs/proposals/ch1tty-connector-revision.md, docs/proposals/chittyledger-naming-plan.md — are well-structured and clearly written. A few observations:

  • mercury-multitenant.md correctly surfaces a CRITICAL schema gap: legal_person_chittyid is absent from accounts in database/system.schema.ts. The deploy gate at the bottom captures it, but this is a real blocker for reconciliation — whoever picks up the schema cutover should reference this doc.

  • chittybooks-chittyfinance.md calls out the books.chitty.cc dead domain clearly, and the three operator options (Cloud Run / Worker rewrite / merged UI) are well-scoped. The "proposed, not yet mounted" annotations on reconciliation endpoints are valuable — they prevent consumers from treating the contract doc as a live API reference.

  • api-v1-prefix-fix.md is a resolved no-action doc — fine to land, but worth noting it documents a decision already made in CLAUDE.md rather than a pending proposal.


Summary

Area Verdict
pnpm audit fix — approach ✅ Correct; --json + jq is the right pnpm v10 pattern
jq filter logic ✅ Correct semantics
Ignore-list hygiene ⚠️ No expiry enforcement, no per-package attribution
Security debt exposure ⚠️ 28 advisories accepted; follow-up PR should be created before merge
CLAUDE.md update
Documentation files ✅ Well-structured; mercury schema gap correctly flagged as CRITICAL

Recommend: approve with the caveat that a follow-up dep-upgrade issue/PR exists and is referenced in the ignore-list comment before this merges.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 484faf5441

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

|---|---|---|
| Mercury read API | `https://api.mercury.com/api/v1` (direct, OAuth tokens scoped per tenant) | Token issued per `(tenant_id, legal_person_chittyid)` via ChittyConnect |
| Mercury write API | `mercury-proxy` on `chittyserv-dev` (IP-allowlisted by Mercury) | `X-Mercury-Token` per request; proxy is stateless re tenancy |
| Mercury webhooks | `POST /api/webhooks/mercury/:tenantId` on `finance.chitty.cc` (native Mercury receiver, per `server/books/webhooks.ts`; PR #113). Per-tenant HMAC secret keyed at `webhook:mercury:secret:<tenantId>` in KV. The legacy `POST /api/webhooks/mercury` path was the ChittyConnect-normalized shim and is not the native receiver. | `tenant_id` is the path param and is verified by HMAC; `legal_person_chittyid` resolution is **not** performed inside the webhook handler today — it joins downstream via the local account row. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require Mercury webhook secrets before claiming HMAC tenant verification

In tenants that do not yet have webhook:mercury:secret:<tenantId> in KV, server/books/webhooks.ts:215-227 logs a warning and skips signature verification entirely, so the path tenantId is not actually HMAC-verified. This contract says the tenant is verified by HMAC and the deploy gate later says the secret is already shipped, which can lead operators to register/use a tenant without provisioning the KV secret while believing the native Mercury receiver is authenticated; make the gate require fail-closed secrets or document the no-secret fallback explicitly.

Useful? React with 👍 / 👎.

| `/api/properties` | Property list |
| `/api/accounts` | Account list + balances |
| `/api/transactions` | Transaction feed for caller's tenant. Today the mounted handler accepts only `?limit=`; `?accountId=` and `?since=` are accepted on `/api/transactions/export`. Any source/date filter parity is a follow-up, not a current guarantee. |
| `/api/allocations/rules`, `/api/allocations/preview`, `/api/allocations/execute`, `/api/allocations/run` | Allocation rule CRUD + dry-run + execution (per `server/accounting/allocations.ts`). There is no umbrella `/api/allocations` index route. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Point allocation run reads at the plural route

When ChittyBooks follows this contract to read allocation run history, /api/allocations/run will 404 because the mounted handler is GET /api/allocations/runs in server/accounting/allocations.ts:267-268 (and CHITTY.md also documents {rules,preview,execute,runs}). Update the table to the plural path so clients do not build against a non-existent endpoint.

Useful? React with 👍 / 👎.

| `/api/properties` | Property list |
| `/api/accounts` | Account list + balances |
| `/api/transactions` | Transaction feed for caller's tenant. Today the mounted handler accepts only `?limit=`; `?accountId=` and `?since=` are accepted on `/api/transactions/export`. Any source/date filter parity is a follow-up, not a current guarantee. |
| `/api/allocations/rules`, `/api/allocations/preview`, `/api/allocations/execute`, `/api/allocations/run` | Allocation rule CRUD + dry-run + execution (per `server/accounting/allocations.ts`). There is no umbrella `/api/allocations` index route. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mount allocation routes under protected middleware before documenting them

The advertised allocation routes are not included in server/app.ts's protectedPrefixes list, so storageMiddleware and tenantMiddleware do not run for /api/allocations/*; the handlers then read c.get('storage')/c.get('tenantId') in server/accounting/allocations.ts, which means ChittyBooks requests to these documented endpoints will fail without storage/tenant context rather than performing CRUD. Add the allocation prefix to the protected middleware list before treating this as a consumable API surface.

Useful? React with 👍 / 👎.


| Source | Path | Tenant binding |
|---|---|---|
| Mercury read API | `https://api.mercury.com/api/v1` (direct, OAuth tokens scoped per tenant) | Token issued per `(tenant_id, legal_person_chittyid)` via ChittyConnect |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route Mercury reads through ChittyConnect

This contract tells implementers that Mercury reads use the direct api.mercury.com API with tenant-scoped OAuth tokens, but the repo’s own Mercury route sends reads through ChittyConnect (server/routes/mercury.ts:18-27) and the agent registry says every bank API call routes through chittyagent-connect. Following the direct path bypasses the ChittyConnect credential/static-egress boundary and can leave new Mercury readers unable to run in environments that only permit the proxy path; document ChittyConnect as the read source instead.

Useful? React with 👍 / 👎.

chitcommit added a commit that referenced this pull request Jun 4, 2026
chore(deps): shrink dep-audit accept-list via overrides

Add pnpm.overrides to force-resolve vulnerable transitive deps to patched
versions. Reduces chittyfinance prod audit from 11 advisories (2 high,
8 moderate, 1 low) to 1 high — only drizzle-orm remains, deferred to
avoid 0.39 -> 0.45 framework jump.

Overrides added (old -> new):
- axios:             1.13.5  -> 1.17.0   eliminates CVE-2026-42043/42033/42035/42264/44492/44495/44494
- @hono/node-server: 1.19.10 -> 1.19.13  eliminates CVE-2026-39406
- hono:              4.12.4  -> 4.12.23  eliminates 5x moderate hono CVEs
- fast-uri:          3.1.0   -> 3.1.2    eliminates CVE-2026-6321/6322
- path-to-regexp:    8.3.0   -> 8.4.2    eliminates CVE-2026-4926
- picomatch:         4.0.3   -> 4.0.4    eliminates CVE-2026-33671
- lodash:            4.17.21 -> 4.18.1   eliminates CVE-2026-4800/2950, CVE-2025-13465
- qs:                6.14.1  -> 6.15.2   eliminates CVE-2026-2391/8723
- ajv:               8.17.x  -> 8.20.0   eliminates CVE-2025-69873
- postcss:           8.5.6   -> 8.5.15   eliminates CVE-2026-41305
- ip-address:        10.1.0  -> 10.1.1   eliminates CVE-2026-42338
- ws:                8.19.0  -> 8.21.0   eliminates CVE-2026-45736

No application code changed. `npm run check` (tsc) passes clean.

Remaining: drizzle-orm CVE-2026-39356 (high). Requires bumping 0.39 -> 0.45,
a major framework upgrade — deferred to its own PR per CLAUDE.md guidance
on framework deps.

Parallel to #124. Once this lands and CI confirms the audit is clean, the
accept-list entries for the CVEs listed above can be pruned from
security-gates.yml.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai review

Please evaluate:

  • Security implications
  • Credential exposure risk
  • Dependency supply chain concerns
  • Breaking API changes

@claude

claude Bot commented Jun 4, 2026

Copy link
Copy Markdown

Code Review — PR #124: fix(ci): pnpm-compatible CVE ignore in dependency audit

Overview

This PR fixes a real CI regression: pnpm audit --ignore <CVE> is an npm flag that pnpm v10 silently rejects with Unknown option: 'ignore', causing the Security Gates audit step to pass vacuously — all advisories were hidden. The fix replaces it with pnpm audit --prod --json piped through a jq post-filter that whitelists accepted advisory IDs. The PR also updates CLAUDE.md with API prefix clarification and adds five docs-only files under docs/contracts/ and docs/proposals/.


Security Gates workflow (.github/workflows/security-gates.yml)

What's correct:

  • The approach (--json + jq) is the documented pnpm v10 pattern. The old flag was a no-op; this is correct.
  • Both CVE and GHSA IDs are listed per advisory — resilient against advisory DB renumbering.
  • set +e / set -e around the audit call correctly handles the expected non-zero exit without killing the step.
  • --ignore-registry-errors is preserved from the original.
  • --audit-level high is gone from the CLI call, which is fine — the jq filter does this explicitly with select(.value.severity == "high" or .value.severity == "critical").

Issues:

1. set +e is too broad — masks pnpm failures unrelated to advisories.

set +e
AUDIT_JSON="$(pnpm audit --prod --json --ignore-registry-errors)"
set -e

If pnpm itself fails (network error, missing lockfile, OOM), AUDIT_JSON will be empty or contain an error message, set -e re-enables, and then jq fails on the malformed input. The step exits non-zero but with a confusing jq error, not an auditable message. A safer pattern captures the exit code explicitly:

AUDIT_JSON="$(pnpm audit --prod --json --ignore-registry-errors)" || AUDIT_EXIT=$?
# Then verify AUDIT_EXIT is 1 (advisories found) vs 2+ (pnpm itself failed)

Or at minimum use || true to make the intent explicit and avoid the set +e / set -e dance entirely.

2. jq all(. == null) logic is subtly correct but undocumented.

| map(. as $id | $ignored | index($id))
| all(. == null)

index($id) returns null when not found (not 0). So all(. == null) means "no ID from this advisory appears in the ignore list" — the advisory is kept (reported). This is the right behaviour, but a reader seeing all(. == null) without context might expect it to mean "all IDs are missing/null" (confusing). A short comment like # all(. == null) = none of this advisory's IDs are in the ignore list would prevent future edits from accidentally flipping the condition.

3. The ignore list size is the real finding here.

The PR description is honest about this: the broken gate hid ~40 CVE/GHSA pairs accumulated against 28 distinct advisories. The PR makes them visible, which is correct. However:

  • There are several 2026-dated CVEs (CVE-2026-33671/33672, CVE-2026-33937–33941, CVE-2026-42033–44705, etc.) on packages like handlebars, minimist, moment, tmp, form-data, fast-uri. These are real advisories on production-reachable transitive deps — their dates just reflect the current year.
  • There is no expiry or rotation mechanism on the ignore list. Once a CVE is added here it will stay until a human removes it. Recommend adding a comment block with a target PR number or date for each group of ignored IDs so the follow-up work is traceable.
  • gh@2.8.9 is called out in the PR description as the primary source (handlebars/marked/minimist). If gh is a dev/CI-only dependency (--prod would exclude it), those advisories should not appear in --prod output. Worth verifying that gh is in devDependencies so the gate isn't silencing prod-reachable issues.

4. Minor: error message has a typo.

echo "::error::$COUNT high/critical advisor(ies) beyond the accepted ignore list:"

advisor(ies)advisor**y/ies** or just advisories. Trivial but it's in a CI error message.


CLAUDE.md

Clean documentation update clarifying the /api/<resource> vs /api/v1/* split. The expanded routes description is accurate and useful. No issues.


New docs (docs/contracts/, docs/proposals/)

All five files are docs-only. They are well-structured with verified-on dates, explicit deploy gates, and clear distinction between "canon today" and "proposed." Specific observations:

  • mercury-multitenant.md correctly flags the CRITICAL schema gap (legal_person_chittyid missing from accounts). No code change is included, which is appropriate given the CLAUDE.md rule against destructive drizzle-kit push. The two remediation paths (schema migration vs metadata interim) are clear. The deploy gate [ ] BLOCKER notation matches the project's existing gate conventions.
  • chittybooks-chittyfinance.md appropriately calls out the books.chitty.cc NXDOMAIN and the charter/code mismatch in CHITTYAPPS/chittybooks. The "operator-gated" framing for deploy options A/B/C is correct.
  • ch1tty-connector-revision.md and chittyledger-naming-plan.md are actionable proposals with concrete file references. No concerns.

Summary

Area Verdict
Core CI fix (jq filter) ✅ Correct — approach is right, logic is sound
set +e / set -e scope ⚠️ Too broad — recommend explicit exit-code capture
jq all(. == null) ⚠️ Needs a comment — correct but non-obvious
Ignore list size ⚠️ Expected given the broken gate; needs a follow-up tracking mechanism (dates/PR refs per group)
--prod vs gh advisory scope ❓ Verify gh is in devDependencies so prod-reachable isolation is clean
CLAUDE.md update
New docs ✅ Well-structured, gated correctly

The core fix is sound and the PR description is unusually honest about the debt it's making visible. The three items above are worth addressing before merge — especially the set +e scope, since a misconfigured pnpm run could now silently produce a passing gate for the wrong reason (empty JSON → jq error → non-zero exit, correct, but confusing and non-auditable).

chitcommit and others added 3 commits June 4, 2026 03:15
The audit step used npm's `--ignore <CVE>` flag, which pnpm v10 rejects with
`Unknown option: 'ignore'`, breaking the gate on every PR.

Replace with `pnpm audit --prod --json` post-filtered through jq:
  - Captures JSON regardless of pnpm's non-zero exit on findings
  - Drops advisories whose CVE list intersects an explicit accept-list
  - Fails the step if any high/critical advisory remains, printing the
    offending module/CVE/url for debuggability

Accept-list covers the picomatch ReDoS finding the original step intended
to ignore. The advisory was renumbered in the GitHub Advisory DB from
CVE-2024-45296 to CVE-2026-33671 / CVE-2026-33672; all three IDs are
listed so the ignore survives further renumbering. Parents (neonctl,
tailwindcss) pin the vulnerable picomatch version, so pnpm overrides
are not viable.

The gate now correctly surfaces ~28 unrelated high/critical advisories
that accumulated while the flag-error masked the audit. Those require
a separate dependency-upgrade pass; this PR only restores the gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dvisories

The previously-broken `pnpm audit --ignore <CVE>` flag (npm syntax, not pnpm)
silently hid 28 pre-existing high+ advisories in transitive deps. The earlier
fix in this PR switched to a `pnpm audit --json` + jq post-filter, but the
accept-list still only covered the original picomatch entries.

This commit expands the accept-list to every high+ advisory currently surfaced
by `pnpm audit --prod --json` against pnpm-lock.yaml, grouped by module:

  - axios x7, handlebars x7, undici x3, fast-uri x2, marked x2, moment x2,
    form-data, lodash, minimist, path-to-regexp, tmp, picomatch

This is TIME-BOUNDED tech-debt acceptance — the path to shrink the list is the
parallel dep-upgrade PR (see follow-up dep-upgrade PR). Every id below is a
real advisory in pnpm's audit output; both CVE and GHSA ids are listed and the
jq filter matches either, so advisory-DB renumbering won't silently re-open an
accepted item.

Local validation: 0 remaining high/critical advisories after filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ept-list

path-to-regexp CVE-2026-4867 / GHSA-37ch-88jc-xwx2 is a sibling
advisory to the already-accepted CVE-2026-4926 on the same module.

drizzle-orm CVE-2026-39356 / GHSA-gpj5-g38j-94v9 is accepted pending
the major version bump tracked in the follow-up PR #125.

Local pnpm audit --prod --json filter confirms REMAINING=0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chitcommit chitcommit force-pushed the fix/ci/pnpm-audit-ignore-cve branch from dba2a8b to 63393ee Compare June 4, 2026 03:15
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai review

Please evaluate:

  • Security implications
  • Credential exposure risk
  • Dependency supply chain concerns
  • Breaking API changes

@claude

claude Bot commented Jun 4, 2026

Copy link
Copy Markdown

Code Review — fix(ci): pnpm-compatible CVE ignore in dependency audit

Overview

This PR fixes a real breakage: pnpm audit --ignore <CVE> is not a valid flag in pnpm v10, so the old command was silently error-exiting on every PR while the gate never actually ran. The fix — capture --json output and post-filter with jq — is the correct pnpm-idiomatic approach. The PR description is unusually transparent about what it's doing and why, which is appreciated.


What's Good

  • Correct root cause fix. Switching to --json + jq is the right solution for pnpm v10.
  • Dual-ID matching (CVE + GHSA). Matching on both identifiers is defensive and correct — advisory-DB renumbering won't silently re-open an accepted item.
  • set +e / set -e pattern around the audit capture is correct; pnpm audit exits non-zero on any advisory, so suppressing the exit code there and then re-enabling it is the right move.
  • cves[]? — the ? suppressor is good defensive jq; handles advisories where cves is absent or null without blowing up.
  • Error output format is actionable — severity, module, CVEs, GHSA, and URL in one line per advisory.

Issues & Suggestions

1. The acceptance list is large and untracked (main concern)

The list jumps from 1 accepted CVE to ~20 accepted advisories. Each entry is accepted silently until someone manually audits the list again. Two improvements:

a) Add a ticket/date comment per group so reviewers can see when each batch was accepted and what's tracking the upgrade:

# Accepted 2026-06-04 — picomatch ReDoS, parent pkgs pin vuln version
# Tracked: follow-up dep-upgrade PR (see PR description)
"CVE-2024-45296","CVE-2026-33671","CVE-2026-33672",
"GHSA-c2c7-rcm5-vvqj",

b) Consider a separate file (e.g. .github/audit-ignore.json) instead of an inline shell variable. This makes the list easier to grep, diff, and review in isolation without scrolling through YAML.

2. jq filter subtlety — null GHSA id

If an advisory has no github_advisory_id, the expression:

[$a.value.cves[]?] + [$a.value.github_advisory_id]

produces ["CVE-xxx", null]. Then $ignored | index(null) returns null (null is not in the list), so all(. == null) is unaffected. This is safe today but fragile — worth a comment.

3. Typo in error message

echo "::error::$COUNT high/critical advisor(ies) beyond the accepted ignore list:"

advisor(ies)advisories. Minor, but this shows in the GitHub Actions UI.

4. No --audit-level flag passed to pnpm

The original had --audit-level high; that flag is dropped here (correctly — the jq filter handles severity). Worth a comment explaining that severity gating moved to jq so a future reader doesn't add it back.

5. --ignore-registry-errors retained — confirm it's still needed

This flag was in the original. Fine to keep, but worth confirming it's still necessary in your environment (it suppresses errors when the npm registry is unreachable during audit).

6. All audit output is fetched, not just high+

pnpm audit --json without --audit-level returns low/moderate/high/critical. For a repo with many transitive deps this JSON can be large. Not a correctness problem, but worth noting if you ever see timeouts or memory issues in the step.


jq Filter Correctness

The core filter logic is correct:

  • to_entries[] — iterates advisory objects
  • select(severity == "high" or "critical") — pre-filters before the expensive ignore check
  • all(. == null) — keeps an advisory only if none of its IDs appear in the ignore list (i.e., drops it if any ID matches) — this is the right semantics

One minor readability note: the . as $a | select(...) pattern is idiomatic jq but the nested $a reference inside the second select can be surprising. A brief comment would help future maintainers.


Summary

Area Assessment
Root cause fix Correct
jq filter logic Correct
Acceptance list size Concern — needs tracking
Documentation Good (PR body explains clearly)
Typo Minor fix needed
Follow-up PR dependency High — 28 high/critical remain

Verdict: Approve with nits. The CI gate was completely broken before; this PR restores it and correctly exposes the accumulated debt. The acceptance list is large but documented. Merge contingent on the follow-up dep-upgrade PR being tracked.

🤖 Reviewed with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 63393ee07e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +109 to +110
"CVE-2022-21680","GHSA-rrrm-qjm4-v8hf",
"CVE-2022-21681","GHSA-5v2h-r2cx-5xgj",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep unrelated high advisories out of the allowlist

When the current dependency tree still contains these non-picomatch high/critical advisories, adding them to IGNORED_IDS makes the audit filter drop them before computing COUNT, so the Dependency Audit job can pass even though the previous gate only attempted to tolerate the picomatch CVE. This turns the high+ audit into an allowlist for all known vulnerabilities in this PR state; keep the ignore list limited to the accepted picomatch IDs if the remaining advisories are supposed to block until upgraded.

Useful? React with 👍 / 👎.

…guard, dual schema, accept-list doc)

Addresses three fail-closed gaps and one comment-rot issue surfaced by the
three-reviewer pass on #124:

1. CRITICAL — Assert pnpm audit JSON shape (has .advisories or .vulnerabilities)
   before jq filtering. Empty/non-JSON/partial output now fails the gate
   instead of being silently treated as "no advisories".
2. IMPORTANT — Null-id guard. Strip nulls from the [cves[] + ghsa] list and
   fail closed on any advisory with no identifier (cannot be matched against
   the accept-list, so cannot be safely ignored).
3. IMPORTANT — Dual-schema support. Filter reads (.advisories // .vulnerabilities)
   so the gate works against both npm v6 / pnpm-legacy and npm v7+ pnpm v10
   audit output shapes.
4. COMMENT ROT — Replace stale "TIME-BOUNDED / see follow-up" preamble with a
   YAML-level table comment grouped by module, referencing tracking issue #126.

Tracking issue: #126

Local verification: synthetic jq fixtures for empty input, non-audit JSON,
accepted CVE (legacy + v7 shapes), null-id advisory, and unaccepted critical
all behave as designed (empty/garbage exit 1; null-id reported as remaining;
accepted ids drop to REMAINING=[]; unaccepted advisories remain).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai review

Please evaluate:

  • Security implications
  • Credential exposure risk
  • Dependency supply chain concerns
  • Breaking API changes

@claude

claude Bot commented Jun 4, 2026

Copy link
Copy Markdown

Code Review — PR #124: fix(ci): pnpm-compatible CVE ignore in dependency audit

Overview

This PR fixes a silent CI breakage: pnpm audit --ignore <CVE> (an npm-only flag) was exiting non-zero on every run, so the Security Gates dependency-audit step was broken. The fix replaces it with pnpm audit --prod --json + a jq post-filter that drops accepted advisories by CVE/GHSA ID. The approach is architecturally correct for pnpm v10.


What Works Well

  • Core fix is correct. set +e / capture / set -e is the right pattern for capturing pnpm audit --json output without aborting the step on the non-zero advisory exit code.
  • Fail-closed on unparseable JSON. If the registry blips or pnpm emits warnings instead of valid JSON, the step errors loudly rather than silently passing. Good defensive posture.
  • Dual-ID matching (CVE + GHSA). Matching on either identifier means advisory-DB renumbering won't silently re-admit a suppressed finding.
  • Well-documented accept-list. The table comment with module, severity, and next-step is clear and actionable.
  • Tracking issue Drive dep-audit accept-list to zero — drizzle major bump + transitive cleanup #126. Having a reference for driving the count to zero is the right way to signal this isn't a permanent suppression.

Bugs

1. join(",") will crash when .cves is null — error output never renders

In the error-reporting path:

jq -r '.[] | "  - [\(.severity)] \(.module) \(.cves | join(",")) \(.ghsa) \(.url)"' <<<"$REMAINING"

If any remaining advisory has a null cves field, join throws and the error message is swallowed — exactly when you most want to see it.

Fix:

jq -r '.[] | "  - [\(.severity)] \(.module) \((.cves // []) | join(",")) \(.ghsa // "no-ghsa") \(.url)"' <<<"$REMAINING"

The null guard on .ghsa is worth adding too — a missing github_advisory_id produces null in the output as-is.

2. .vulnerabilities shape mismatch (npm v7+ / pnpm ≥ 7)

The filter handles the has("vulnerabilities") branch, but then accesses .value.cves[]? and .value.github_advisory_id — fields that live in the .advisories shape. In the npm v7+ .vulnerabilities schema, each entry is keyed by package name with a via[] sub-array for the actual advisory objects; there is no top-level .cves or .github_advisory_id on the entry itself.

In practice pnpm audit --json currently emits the legacy shape, so this is latent. But the has("vulnerabilities") guard implies future-proofing that doesn't actually work yet. Either:

  • Drop the guard and document that only the legacy shape is supported, or
  • Add a proper normalisation step for the v7 shape.

Concerns

3. Accept-list scope expansion (1 CVE → 30+)

The original suppress was one advisory (CVE-2024-45296 / picomatch ReDoS). This PR admits ~30 high-severity findings across axios, handlebars, minimist, moment, marked, semver, tar, ws, form-data, drizzle-orm, and more. The PR description explains these were already silently bypassing the gate — fair — but it means the gate has been effectively off for all of them, and the accept-list now encodes that debt.

Suggestion: Consider adding a creation date comment next to each accepted entry, so future readers know how old each suppression is and can triage accordingly.

4. Forward-dated CVEs with range notation in comments

The comment table uses range notation like CVE-2026-33937..41 and CVE-2026-42033..495 for handlebars and esbuild/postcss. The IGNORED_IDS array expands each ID individually (good), but the comment implies a contiguous range that isn't enforced — if CVE-2026-33942 were assigned, the comment would silently mislead a reader into thinking it's covered. Tighten the table to list only IDs that actually appear in IGNORED_IDS, or add a comment noting the notation is illustrative.

5. AUDIT_JSON stored in a shell variable

For repos with many transitive dependencies, pnpm audit --json output can be several MB. Bash variables handle this, but it's brittle if the shell hits ARG_MAX or the runner runs low on memory. A temp file is more robust:

AUDIT_FILE="$(mktemp)"
pnpm audit --prod --json --ignore-registry-errors >"$AUDIT_FILE" 2>&1 || true
# then use < "$AUDIT_FILE" instead of <<< "$AUDIT_JSON"

Minor / Nit

  • The --ignore-registry-errors flag is preserved — fine, but worth noting in a comment that this masks registry auth failures (acceptable trade-off for CI).
  • The jq null-id guard comment says "fail closed" but the mechanism is that the advisory lands in REMAINING because length == 0 makes the select true. The comment is correct; consider adding a one-line example to make the intent obvious to the next person editing this.

Summary

Area Verdict
Core fix (jq post-filter) ✅ Correct approach
Fail-closed on bad JSON ✅ Good
join(",") on null cves ❌ Bug — fix before merge
.vulnerabilities shape ⚠️ Latent mismatch
Accept-list scope ⚠️ Large — tracking issue #126 is the right mitigation
Forward-range CVE comments ⚠️ Misleading notation
AUDIT_JSON variable 💡 Minor robustness nit

The null-guard bug on join(",") should be fixed before merge — it breaks the only human-readable output path at the moment it matters most. The .vulnerabilities shape issue is worth a follow-up comment or issue. Everything else can land with the understanding that #126 drives the accept-list to zero.


Reviewed by Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/security-gates.yml (1)

60-61: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Pin the pnpm version used by security-gates.yml (jq assumes pnpm audit’s legacy schema). pnpm/action-setup@v4 is unpinned, so the workflow may run with a pnpm major whose pnpm audit --json payload doesn’t include CVE fields. In pnpm v11, the audit output migrated to the bulk advisories endpoint and no longer returns CVE identifiers; GitHub Advisory IDs (GHSA) are used instead. With that change, your jq logic that extracts cves and then prints/joins .cves can fail to match the accept list and/or produce incorrect output. Pin pnpm explicitly in the action (e.g., version:) or update the jq to handle both schemas by preferring GHSA and guarding .cves when absent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/security-gates.yml around lines 60 - 61, The workflow
currently uses the unpinned action reference pnpm/action-setup@v4 which can
install different pnpm majors; update the step that uses pnpm/action-setup@v4 to
pin a specific pnpm version (via the action's version: or version input) to a
pnpm release that still exposes .cves (or choose a supported major), or
alternatively modify the jq extraction logic that reads audit JSON (the code
referencing .cves and joins .cves) to handle both schemas by first preferring
GHSA fields and falling back to .cves only when present (guarding access to
.cves to avoid failures).
🧹 Nitpick comments (1)
.github/workflows/security-gates.yml (1)

122-122: 💤 Low value

Optional: bind the step input to an env var instead of inlining ${{ }} in run. Static analysis flags the ${{ steps.pkg.outputs.dir }} expansion as template injection. Here the value is constrained to fixed, self-produced strings (., development/chittyschema, or empty), so it's effectively a false positive — but binding via env: is the idiomatic GitHub Actions hardening and silences the linter.

♻️ Suggested pattern
      - name: Enforce audit high threshold
        if: steps.pkg.outputs.dir != ''
        env:
          PKG_DIR: ${{ steps.pkg.outputs.dir }}
        run: |
          set -euo pipefail
          cd "$PKG_DIR"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/security-gates.yml at line 122, The run step uses an
inlined GitHub Actions expression (${ { steps.pkg.outputs.dir } }) which static
analysis flags; change the step to bind that output to an environment variable
(e.g., PKG_DIR) via the step's env: block and then use the env var in the run
script (cd "$PKG_DIR") so the value comes from PKG_DIR instead of direct
template expansion; update the step that currently references
steps.pkg.outputs.dir to set env: PKG_DIR: ${{ steps.pkg.outputs.dir }} and
change the run command to cd "$PKG_DIR".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/security-gates.yml:
- Line 213: The jq formatter currently runs `.cves | join(",")` which fails when
`.cves` is null; update the jq expression used in the line that builds the
report (the pipeline using `.[] | "  - [\(.severity)] \(.module) \(.cves |
join(",")) \(.ghsa) \(.url)"'`) so that `.cves` is defaulted to an empty array
when null (i.e., coalesce or fallback to [] before calling join), ensuring null
`cves` produce an empty string instead of raising an error.

---

Outside diff comments:
In @.github/workflows/security-gates.yml:
- Around line 60-61: The workflow currently uses the unpinned action reference
pnpm/action-setup@v4 which can install different pnpm majors; update the step
that uses pnpm/action-setup@v4 to pin a specific pnpm version (via the action's
version: or version input) to a pnpm release that still exposes .cves (or choose
a supported major), or alternatively modify the jq extraction logic that reads
audit JSON (the code referencing .cves and joins .cves) to handle both schemas
by first preferring GHSA fields and falling back to .cves only when present
(guarding access to .cves to avoid failures).

---

Nitpick comments:
In @.github/workflows/security-gates.yml:
- Line 122: The run step uses an inlined GitHub Actions expression (${ {
steps.pkg.outputs.dir } }) which static analysis flags; change the step to bind
that output to an environment variable (e.g., PKG_DIR) via the step's env: block
and then use the env var in the run script (cd "$PKG_DIR") so the value comes
from PKG_DIR instead of direct template expansion; update the step that
currently references steps.pkg.outputs.dir to set env: PKG_DIR: ${{
steps.pkg.outputs.dir }} and change the run command to cd "$PKG_DIR".
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5777374f-eebe-4320-8271-7bb1d7c0c8f5

📥 Commits

Reviewing files that changed from the base of the PR and between 4fdfc29 and ff4ee76.

📒 Files selected for processing (1)
  • .github/workflows/security-gates.yml

COUNT="$(jq 'length' <<<"$REMAINING")"
if [ "$COUNT" -gt 0 ]; then
echo "::error::$COUNT high/critical advisor(ies) beyond the accepted ignore list:"
jq -r '.[] | " - [\(.severity)] \(.module) \(.cves | join(",")) \(.ghsa) \(.url)"' <<<"$REMAINING"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard .cves against null in the report. If a retained advisory has no cves array (null-id advisory at line 204, or any non-legacy shape), .cves | join(",") raises a jq error instead of printing the intended diagnostic, obscuring the real failure.

🛡️ Proposed fix
-          jq -r '.[] | "  - [\(.severity)] \(.module) \(.cves | join(",")) \(.ghsa) \(.url)"' <<<"$REMAINING"
+          jq -r '.[] | "  - [\(.severity)] \(.module) \((.cves // []) | join(",")) \(.ghsa) \(.url)"' <<<"$REMAINING"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
jq -r '.[] | " - [\(.severity)] \(.module) \(.cves | join(",")) \(.ghsa) \(.url)"' <<<"$REMAINING"
jq -r '.[] | " - [\(.severity)] \(.module) \((.cves // []) | join(",")) \(.ghsa) \(.url)"' <<<"$REMAINING"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/security-gates.yml at line 213, The jq formatter currently
runs `.cves | join(",")` which fails when `.cves` is null; update the jq
expression used in the line that builds the report (the pipeline using `.[] | " 
- [\(.severity)] \(.module) \(.cves | join(",")) \(.ghsa) \(.url)"'`) so that
`.cves` is defaulted to an empty array when null (i.e., coalesce or fallback to
[] before calling join), ensuring null `cves` produce an empty string instead of
raising an error.

@mrniceweird mrniceweird marked this pull request as draft June 4, 2026 04:03
auto-merge was automatically disabled June 4, 2026 04:03

Pull request was converted to draft

@mrniceweird mrniceweird self-requested a review June 4, 2026 04:03
@mrniceweird mrniceweird marked this pull request as ready for review June 4, 2026 04:03
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai review

Please evaluate:

  • Security implications
  • Credential exposure risk
  • Dependency supply chain concerns
  • Breaking API changes

@mrniceweird mrniceweird merged commit e296809 into main Jun 4, 2026
14 of 15 checks passed
@mrniceweird mrniceweird deleted the fix/ci/pnpm-audit-ignore-cve branch June 4, 2026 04:04
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 2 file(s) based on 1 unresolved review comment.

A stacked PR containing fixes has been created.

  • Stacked PR: #127
  • Files modified:
  • package.json
  • tsconfig.json

Time taken: 2m 42s


⚠️ 1 file(s) could not be committed — the agent does not have permission to push to .github/workflows/. Please apply these changes manually:

.github/workflows/security-gates.yml — 1 change:

Lines 210–216
           COUNT="$(jq 'length' <<<"$REMAINING")"
           if [ "$COUNT" -gt 0 ]; then
             echo "::error::$COUNT high/critical advisor(ies) beyond the accepted ignore list:"
-            jq -r '.[] | "  - [\(.severity)] \(.module) \(.cves | join(",")) \(.ghsa) \(.url)"' <<<"$REMAINING"
+            jq -r '.[] | "  - [\(.severity)] \(.module) \((.cves // []) | join(",")) \(.ghsa) \(.url)"' <<<"$REMAINING"
             exit 1
           fi
           echo "No high/critical advisories beyond the accepted ignore list."

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the security-gates workflow file
fd -e yml -e yaml . .github/workflows/ | xargs cat

echo "---AUDIT JQ LOGIC CHECK---"
# Check if there are any test/fixture files added
fd -e json . .github/ 2>/dev/null | head -20
fd fixture . 2>/dev/null | head -20
fd audit . 2>/dev/null | head -20

Length of output: 38183



@chitcommit — here is the structured security review of PR #124.


Security Review

1. Security Implications

✅ Fail-closed behavior is correct. The jq select(($ids | length) == 0 or (...)) guard ensures advisories with no CVE and no GHSA ID land in REMAINING, not silently pass — this is the right behavior.

✅ Dual-schema support (.advisories // .vulnerabilities) correctly handles both legacy pnpm/npm audit shapes.

⚠️ --ignore-registry-errors creates a silent pass-through gap. If the npm registry is unreachable during CI, pnpm audit may return an empty or partial result set. The JSON-shape assertion (has("advisories") or has("vulnerabilities")) should catch a completely empty response, but a partial response (e.g., registry timeout mid-stream) could produce a syntactically valid but incomplete JSON object that passes the gate. Consider failing if the advisory count is suspiciously low (e.g., zero total advisories when transitive deps are known to be present).

⚠️ Undocumented entry in IGNORED_IDS: CVE-2026-4867 / GHSA-37ch-88jc-xwx2 appears in the accept-list but is absent from both the PR comment table and the inline documentation table. This entry should be disclosed with module name, severity, and rationale just like the other 28 entries.

⚠️ Module-name mismatches between the inline comment table and the PR description table. At minimum six rows disagree on which module a CVE belongs to — one of the two sources is incorrect. Since the module name isn't part of the jq matching logic (only CVE/GHSA IDs matter), this doesn't break filtering, but it undermines auditability and makes the tracking issue harder to act on:

CVE PR description says Inline workflow comment says
CVE-2026-1526/1528/2229 undici semver
CVE-2026-6321/6322 fast-uri tar
CVE-2026-42033..495 axios esbuild/postcss
CVE-2026-4800 lodash micromatch
CVE-2026-4926 path-to-regexp tough-cookie
CVE-2026-44705 tmp ws

⚠️ Accept-list has no integrity protection. Any subsequent PR can silently expand IGNORED_IDS without a separate approver noticing. The tracking issue (#126) referenced in the inline comment is the right human control here, but consider adding a CODEOWNERS entry for .github/workflows/security-gates.yml to require explicit review of changes to that file.


2. Credential Exposure Risk

No credential exposure in this PR's changes. The dependency-audit step emits only advisory metadata to stdout.

⚠️ Pre-existing concern in chittyconnect-sync workflow (out of scope for this PR but worth flagging):

-d '{"repository":"${{ github.repository }}","commit":"${{ github.sha }}"}'

${{ ... }} is interpolated by GitHub Actions before the shell sees the string. While github.sha and github.repository are not secrets, this is the same pattern that would be dangerous if a secret were ever inline-interpolated. The canonical mitigation is to pass values via environment variables and reference them as $REPO / $SHA in the shell string. Low severity here, but worth correcting for consistency with the secret-policy gate this repo already enforces.


3. Dependency Supply Chain Concerns

--prod flag — only production deps are audited. Dev-dependency vulnerabilities (e.g., in bundlers, test runners, linters) will not surface here. This is a conscious scope decision but worth documenting in the inline comment so future maintainers don't wonder why dev-dep advisories are missing.

pnpm/action-setup@v4 and actions/setup-node@v4 — both are unpinned to a SHA. For a security-gate workflow specifically, SHA-pinning these actions is recommended so a compromised action tag cannot inject malicious behavior into the gate itself.

External fetch in governance.yml — downloads rules.json and CANON_PUBLIC_KEY from raw.githubusercontent.com/chittyfoundation/.github/main at runtime. The signature verification step is the integrity control, but the fetch is not pinned to a commit SHA — a force-push to main in the chittyfoundation/.github repo would take effect immediately on the next run. This is pre-existing and out of scope for this PR, but noteworthy.


4. Breaking API Changes

None. This is a CI-only change affecting .github/workflows/security-gates.yml. No application code, exported symbols, or public API surfaces are modified.


Summary

Area Status Priority
Fail-closed jq logic ✅ Correct
Undocumented CVE-2026-4867 entry ⚠️ Needs disclosure High
Module-name mismatches in inline docs ⚠️ Audit trail confusion Medium
--ignore-registry-errors partial-output gap ⚠️ Consider total-count floor Medium
Accept-list integrity (CODEOWNERS) ⚠️ Process gap Medium
Credential exposure (this PR) ✅ None
Action SHA-pinning in security-gate workflow ⚠️ Pre-existing Low
Breaking API changes ✅ None

The core mechanism (JSON + jq post-filter, dual-schema, null-id guard) is sound. The main ask before merge is to document CVE-2026-4867 in the PR table and reconcile the module-name discrepancies in the inline comment table so the tracking issue (#126) has an accurate starting inventory.


🧠 Learnings used
Learnt from: CR
Repo: chittyapps/chittyfinance PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-07T14:48:32.602Z
Learning: Keep PRs small and focused; include summary, rationale, and screenshots for UI changes; link related issues; document env/DB changes and run `npm run db:push` when schema updates

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants